﻿//-----------------------------------------------------------------------------
// File: AVCProcessHost.cs
// Author: Pierre Biermann (tauron031@gmail.com)
// Project: FFMPEG audio video converter class library
// Namespace(s): FFShellAPI.FFMPEG
//
// Desc: Represents the main class for calling the ffmpeg audio video 
//       converter command line tools as background child process.
//       
// (c) 2007 - 2013 by Pierre Biermann
//-----------------------------------------------------------------------------

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace FFShellAPI.FFMPEG.Core
{
    /// <summary>
    /// Event argument class that passed to the "EncodingFinished" event of the AVCProcessHost class.
    /// </summary>
    public class EncodingFinishedEventArgs : EventArgs
    {
        string _processOutput;

        /// <summary>
        /// Creates a new EncodingFinishedEventArgs object instance
        /// </summary>
        /// <param name="processOutput">output data of conversion child process</param>
        public EncodingFinishedEventArgs(string processOutput)
        {
            _processOutput = processOutput;
        }

        /// <summary>
        /// Gets the output data of a conversion child process after has been exited.
        /// </summary>
        public string ProcessOutput
        {
            get { return _processOutput; }
        }
    }

    /// <summary>
    /// Class that passing progress data informations from 
    /// active convertion or probing process to the HandleProgressData event of AVCProcessHost class.
    /// </summary>
    public class ProgressDataEventArgs : EventArgs
    {
        string _progressData;

        /// <summary>
        /// Creates a new ProgressDataEventArgs instance.
        /// </summary>
        /// <param name="progressData">string with progress informations from child process</param>
        public ProgressDataEventArgs(string progressData)
        {
            _progressData = progressData;
        }

        /// <summary>
        /// Gets the progress data from the child process thats running in background.
        /// </summary>
        public string ProgressData
        {
            get { return _progressData; }
        }
    }

    /// <summary>
    /// Exception class for handling converting errors.
    /// </summary>
    public class AVConvertingError : Exception
    {
        /// <summary>
        /// Creates a new exeception.
        /// </summary>
        public AVConvertingError()
            : base()
        {
        }

        /// <summary>
        /// Creates a new exeception.
        /// </summary>
        /// <param name="message">error message</param>
        public AVConvertingError(string message)
            : base(message)
        {
        }
    }

    /// <summary>
    /// Class that provides a special implementation of the StringWriter.Write(string) method 
    /// for building command lines that can be passed to child process calls.
    /// This class is used by all audio video settings classes for building the command line string internally.
    /// </summary>
    public class CommandLineWriter : StringWriter
    {
        /// <summary>
        /// Write method that first inserts a whitespace charecter 
        /// if the current string dosen't ends with them.
        /// </summary>
        /// <param name="value">string to write into the stream</param>
        public override void Write(string value)
        {
            string cmd = ToString();
            if (!String.IsNullOrWhiteSpace(cmd))
            {
                if (cmd[cmd.Length - 1] != ' ')
                {
                    Write(' ');
                }
            }
            base.Write(value);
        }

        /// <summary>
        /// Method to encapsulate a file name in the command line.
        /// </summary>
        /// <param name="fileName">file name</param>
        public void WriteFileArgument(string fileName)
        {
           Write("\"" + fileName + "\"");
        }
    }

    /// <summary>
    /// Event handler for the EncodingFinished event of AVCProcessHost class.
    /// </summary>
    /// <param name="sender">Sender object (should be an AVCProcessHost instance)</param>
    /// <param name="args">EncodingFinishedEventArgs instance</param>
    public delegate void EncodingFinishedEventHandler(object sender, EncodingFinishedEventArgs args);

    /// <summary>
    /// Event handler for the HandleProgressData event of AVCProcessHost class.
    /// </summary>
    /// <param name="sender">Sender object (should be an AVCProcessHost instance)</param>
    /// <param name="args">ProgressDataEventArgs instance</param>
    public delegate void HandleProgressDataEventHandler(object sender, ProgressDataEventArgs args);

    /// <summary>
    /// Main class for calling the ffmpeg audio video 
    /// converter command line tools as a background child process.
    /// </summary>
    public class AVCProcessHost
    {
        #region private fields
        bool runningState;
        FileInfo ffmpeg;
        FileInfo ffprobe;
        FileInfo ffplay;
        StringWriter outputBuffer;
        Process currentProcess;
        #endregion

        #region public constructors
        /// <summary>
        /// Creates a new AVCProcessHost object instance.
        /// </summary>
        public AVCProcessHost()
        {
            runningState = false;
            ffmpeg = new FileInfo("ffmpeg.exe");
            ffprobe = new FileInfo("ffprobe.exe");
            ffplay = new FileInfo("ffplay.exe");
        }

        /// <summary>
        /// Creates a new AVCProcessHost object instance with a path parameter 
        /// that points to the ffmpeg executable directory.
        /// </summary>
        /// <param name="executablePath">path to ffmpeg executables</param>
        public AVCProcessHost(string executablePath)
        {
            runningState = false;
            ffmpeg = new FileInfo(executablePath + @"\ffmpeg.exe");
            ffprobe = new FileInfo(executablePath + @"\ffprobe.exe");
            ffplay = new FileInfo(executablePath + @"\ffplay.exe");
        } 
        #endregion

        #region private methods
        private void CheckForValidPath(bool checkFFmpeg, bool checkFFprobe, bool checkFFplay)
        {
            if (checkFFmpeg)
            {
                if (!ffmpeg.Exists)
                    throw new AVConvertingError("could't find ffmpeg.exe");
            }
            if (checkFFprobe)
            {
                if (!ffprobe.Exists)
                    throw new AVConvertingError("could't find ffprobe.exe");
            }
            if (checkFFplay)
            {
                if (!ffprobe.Exists)
                    throw new AVConvertingError("could't find ffplay.exe");
            }
        } 
        #endregion

        #region public methods
        /// <summary>
        /// Starts a new convertion process with the specified arguments.
        /// </summary>
        /// <param name="arguments">arguments for the command line that passed to ffmpeg.exe</param>
        /// <param name="outputFileName">output file name</param>
        /// <exception cref="AVConvertingError">
        /// Can be thrown in case of any failure during the convertion process or 
        /// before in case of another convertion or probing process is running at same time.
        /// </exception>
        public void ConvertMedia(string outputFileName, params object[] arguments)
        {
            if (runningState)
                throw new AVConvertingError("Converting already in progress.");
            CheckForValidPath(true, false, false);
            CommandLineWriter cmdLine = new CommandLineWriter();
            foreach (object arg in arguments)
            {
                cmdLine.Write(arg.ToString());
            }
            cmdLine.WriteFileArgument(outputFileName);
            Debug.WriteLine(cmdLine.ToString());
            currentProcess = Execute(ffmpeg.FullName, cmdLine.ToString());
        }

        /// <summary>
        /// Writes the given conversion settings to the given output stream. 
        /// This method can be used to save a complete conversion chain to a batch file.
        /// </summary>
        /// <param name="output">output stream</param>
        /// <param name="outputFileName">output filename (conversion target)</param>
        /// <param name="arguments">convertion arguments</param>
        public void SaveConversionBatch(StreamWriter output, string outputFileName, params object[] arguments)
        {
            CommandLineWriter cmdLine = new CommandLineWriter();
            foreach (object arg in arguments)
            {
                cmdLine.Write(arg.ToString());
            }
            cmdLine.WriteFileArgument(outputFileName);
            output.Write(ffmpeg);
            output.Write(' ');
            output.WriteLine(cmdLine.ToString());
        }

        /// <summary>
        /// Starts a new probing process with the specified arguments.
        /// </summary>
        /// <param name="arguments">arguments for the command line that passed to ffmpeg.exe</param>
        /// <exception cref="AVConvertingError">
        /// Can be thrown in case of any failure during the convertion process or 
        /// before in case of another convertion or probing process is running now.
        /// </exception>
        public void ProbeMedia(params object[] arguments)
        {
            if (runningState)
                throw new AVConvertingError("Converting already in progress.");
            CheckForValidPath(false, true, false);
            CommandLineWriter cmdLine = new CommandLineWriter();
            foreach (object arg in arguments)
            {
                cmdLine.Write(arg.ToString());
            }
            Debug.WriteLine(cmdLine.ToString());
            currentProcess = Execute(ffprobe.FullName, cmdLine.ToString());
        }

        /// <summary>
        /// Starts a new playing process with the specified arguments.
        /// </summary>
        /// <param name="arguments">arguments for the command line that passed to ffmpeg.exe</param>
        /// <exception cref="AVConvertingError">
        /// Can be thrown in case of any failure during the convertion process or 
        /// before in case of another convertion or probing process is running now.
        /// </exception>
        public void PlayMedia(params object[] arguments)
        {
            if (runningState)
                throw new AVConvertingError("Converting already in progress.");
            CheckForValidPath(false, false, true);
            CommandLineWriter cmdLine = new CommandLineWriter();
            foreach (object arg in arguments)
            {
                cmdLine.Write(arg.ToString());
            }
            Debug.WriteLine(cmdLine.ToString());
            currentProcess = Execute(ffplay.FullName, cmdLine.ToString());
        }

        /// <summary>
        /// Terminates a currently running convertion, probing or 
        /// playing process instantly by calling the Process.Kill method.
        /// </summary>
        public void TerminateProcess()
        {
            if (currentProcess != null)
            {
                if (!currentProcess.HasExited)
                {
                    currentProcess.Kill();
                    currentProcess = null;
                }
            }
        }
        #endregion

        #region protected methods
        /// <summary>
        /// Fills the process startup informations for calling of any converting programms.
        /// </summary>
        /// <param name="fileName">the file name to the executable that should be run.</param>
        /// <param name="args">command line arguments that should be passed to the executing process.</param>
        /// <param name="redirectOutput">true if the process outputs are redirected in seperate streams</param>
        /// <returns>Process object instance that contains all necessary settings for calling the convertion child process</returns>
        protected virtual Process CreateProcess(string fileName, string args, bool redirectOutput)
        {
            // filling process startup informations
            ProcessStartInfo psi = new ProcessStartInfo(fileName, args);
            psi.CreateNoWindow = true;
            psi.RedirectStandardOutput = redirectOutput;
            psi.RedirectStandardError = redirectOutput;
            psi.UseShellExecute = false;
            psi.StandardOutputEncoding = Encoding.ASCII;
            psi.StandardErrorEncoding = Encoding.ASCII;
            
            // create and fills process data
            Process childProcess = new Process();
            childProcess.StartInfo = psi;
            outputBuffer = null;
            if (redirectOutput)
            {
                outputBuffer = new StringWriter();
                childProcess.OutputDataReceived += new DataReceivedEventHandler(childProcess_DataReceived);
                childProcess.ErrorDataReceived += new DataReceivedEventHandler(childProcess_DataReceived);
            }

            return childProcess;
        }

        /// <summary>
        /// Message handler process outputs
        /// </summary>
        /// <param name="sender">sender object</param>
        /// <param name="e">DataReceivedEventArgs object instance</param>
        protected virtual void childProcess_DataReceived(object sender, DataReceivedEventArgs e)
        {
            if (outputBuffer != null && !String.IsNullOrWhiteSpace(e.Data))
            {
                outputBuffer.WriteLine(e.Data);
                OnHandleProgressData(e.Data);
            }
        }

        /// <summary>
        /// Execute a child process in background with the sepecifed command line arguments.
        /// </summary>
        /// <param name="fileName">file name of the executable that should be run</param>
        /// <param name="args">command line arguments that should be passed to the call</param>
        /// <returns>a process object instance with all necessary settings 
        /// but without a calling to the Process.Start() method</returns>
        protected Process Execute(string fileName, string args)
        {
            Process avc = CreateProcess(fileName, args, true);
            Action<object> exec = (object pso) =>
            {
                outputBuffer = new StringWriter();
                runningState = true;
                Process ps = pso as Process;
                OnBeginEncoding();
                ps.Start();
                ps.BeginErrorReadLine();
                ps.BeginOutputReadLine();
                ps.WaitForExit();
                runningState = false;
                OnEncodingFinished();
            };
            Task.Factory.StartNew(exec, avc);
            return avc;
        }

        /// <summary>
        /// This method was calling after enconding process has been finished.
        /// </summary>
        protected virtual void OnEncodingFinished()
        {
            if (EncodingFinished != null)
            {
                if (outputBuffer != null)
                {
                    EncodingFinished(this, new EncodingFinishedEventArgs(outputBuffer.ToString()));
                }
                else
                {
                    EncodingFinished(this, new EncodingFinishedEventArgs(String.Empty));
                }
            }
        }

        /// <summary>
        /// This method is calling before encoding process has been started.
        /// </summary>
        protected virtual void OnBeginEncoding()
        {
            if (BeginEncoding != null)
            {
                BeginEncoding(this, new EventArgs());
            }
        }

        /// <summary>
        /// Calls the HandleProgressData event handler.
        /// </summary>
        /// <param name="data">progress data from child process</param>
        protected virtual void OnHandleProgressData(string data)
        {
            if (HandleProgressData != null)
            {
                HandleProgressData(this, new ProgressDataEventArgs(data));
            }
        }
        #endregion

        #region public propertys
        /// <summary>
        /// Returns true if a convertion is in progress.
        /// </summary>
        public bool ConvertionInProgress
        {
            get { return runningState; }
        }

        /// <summary>
        /// Sets or gets the file path to the "ffmeg.exe" file.
        /// </summary>
        public string FFMPegPath
        {
            get
            {
                return ffmpeg.FullName;
            }
            set
            {
                ffmpeg = new FileInfo(value);
            }
        }

        /// <summary>
        /// Sets or gets the file path to the "ffprobe.exe" file.
        /// </summary>
        public string FFProbePath
        {
            get
            {
                return ffprobe.FullName;
            }
            set
            {
                ffprobe = new FileInfo(value);
            }
        }

        /// <summary>
        /// Sets or gets the file path to the "ffplay.exe" file.
        /// </summary>
        public string FFPlayPath
        {
            get
            {
                return ffplay.FullName;
            }
            set
            {
                ffplay = new FileInfo(value);
            }
        } 
        #endregion

        #region public events
        /// <summary>
        /// Event handler thats calling before encoding process has been started.
        /// </summary>
        public event EventHandler BeginEncoding;

        /// <summary>
        /// Event handler thats calling after enconding process has been finished.
        /// </summary>
        public event EncodingFinishedEventHandler EncodingFinished;

        /// <summary>
        /// Event handler thats calling during a convertion or probing process 
        /// whenever the child process sends data via a redirected output stream to the host application. 
        /// </summary>
        public event HandleProgressDataEventHandler HandleProgressData;
        #endregion
    }
}
